package com.lds.config.center.client.service;

import com.lds.config.center.client.common.constant.ClientConstant;
import com.lds.config.center.client.common.enums.ClientErrorEnum;
import com.lds.config.center.client.common.model.request.GetAllItemRequest;
import com.lds.config.center.client.common.model.request.NotifyUpdateItemRequest;
import com.lds.config.center.client.common.model.response.GetAllItemResponse;
import com.lds.config.center.client.common.model.response.NotifyUpdateItemResponse;
import com.lds.config.center.client.common.util.WatchKeyUtil;
import com.lds.config.center.client.listener.*;
import com.lds.management.center.common.model.dto.ResultDTO;
import com.lds.management.center.common.util.AssertUtil;
import com.lds.management.center.common.util.RestUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.FileUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;

import java.io.File;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
public class ClientServiceImpl implements ClientService {
    private static final ClientService INSTANCE = new ClientServiceImpl();

    private final String CACHE_FILE_ROOT_PATH = System.getProperty(ClientConstant.CACHE_FILE_ROOT_PATH);

    private final String URL = System.getProperty(ClientConstant.URL_PROPERTY_NAME);
    private final String ENV_NAME = System.getProperty(ClientConstant.ENV_NAME_PROPERTY_NAME);
    private final String CLUSTER_NAME = System.getProperty(ClientConstant.CLUSTER_NAME_PROPERTY_NAME);
    private final String APP_NAME = System.getProperty(ClientConstant.APP_NAME_PROPERTY_NAME);
    private final String NAMESPACE_NAMES = System.getProperty(ClientConstant.NAMESPACE_NAMES_PROPERTY_NAME);

    private final String ALL_URL = URL + "/api/client/item/get-all";
    private final ParameterizedTypeReference<ResultDTO<GetAllItemResponse>> ALL_REFERENCE = new ParameterizedTypeReference<ResultDTO<GetAllItemResponse>>() {};
    private GetAllItemRequest cacheRequest;

    private final String UPDATE_URL = URL + "/api/client/item/notify-update";
    private final ParameterizedTypeReference<ResultDTO<NotifyUpdateItemResponse>> UPDATE_REFERENCE = new ParameterizedTypeReference<ResultDTO<NotifyUpdateItemResponse>>() {};

    private final ItemChangeEventPublisher publisher = new DefaultItemChangeEventPublisher();

    private ClientServiceImpl() {
        checkProperties();

        publisher.register(new DefaultItemChangeEventListener());
    }

    public static ClientService getInstance() {
        return INSTANCE;
    }

    private void checkProperties() {
        AssertUtil.assertTrue(Objects.nonNull(ENV_NAME), ClientErrorEnum.ENV_NOT_EXIST);
        AssertUtil.assertTrue(Objects.nonNull(CLUSTER_NAME), ClientErrorEnum.CLUSTER_NOT_EXIST);
        AssertUtil.assertTrue(Objects.nonNull(APP_NAME), ClientErrorEnum.APP_NOT_EXIST);
        AssertUtil.assertTrue(Objects.nonNull(NAMESPACE_NAMES), ClientErrorEnum.NAMESPACE_NOT_EXIST);
    }

    @Override
    public ResultDTO<GetAllItemResponse> getAllItem() {
        GetAllItemRequest request = getCacheRequest();

        ResultDTO<GetAllItemResponse> resultDTO = RestUtil.post(ALL_URL, request, ALL_REFERENCE);
        if (!ResultDTO.isSuccess(resultDTO)) {
            log.error("获取所有配置项失败");
            return resultDTO;
        }

        if (Objects.isNull(cacheRequest)) {
            GetAllItemResponse response = resultDTO.getData();
            request.setEnvId(response.getEnvId());
            request.setClusterId(response.getClusterId());
            request.setAppId(response.getAppId());
            request.setNamespaceNameMap(namespaceNameMap(response.getNamespaceMap()));
            cacheRequest = request;
        }

        // 同步到本地文件
        syncToLocalFile(resultDTO.getData());
        return resultDTO;
    }

    private void syncToLocalFile(GetAllItemResponse response) {
        if (Objects.isNull(response)) return;

        response.getNamespaceMap().forEach((namespaceName, namespace) -> {
            List<String> propertiesLines = propertiesLines(namespace.getItems());
            if (CollectionUtils.isNotEmpty(propertiesLines)) {
                String cacheFileName = getCacheFileName(namespaceName);
                File file = new File(cacheFileName);
                try {
                    FileUtils.writeLines(file, propertiesLines);
                } catch (Exception e) {
                    log.warn("syncToLocalFile fail. cause={}", e.getMessage());
                }
            }
        });
    }

    private List<String> propertiesLines(List<GetAllItemResponse.Item> items) {
        if (CollectionUtils.isEmpty(items)) return null;

        return items.stream()
                .map(item -> item.getName() + "=" + item.getValue())
                .collect(Collectors.toList());
    }

    private String getCacheFileName(String namespaceName) {
        return CACHE_FILE_ROOT_PATH + watchKey(namespaceName);
    }

    private String watchKey(String namespaceName) {
        return WatchKeyUtil.buildWatchKey(ENV_NAME, CLUSTER_NAME, APP_NAME, namespaceName);
    }

    @Override
    public Map<String, Map<String, Object>> getAllItemFromLocal() {
        Map<String, Map<String, Object>> cacheItemMap = new HashMap<>();
        for (String namespaceName : NAMESPACE_NAMES.split(",")) {
            Properties localProperties = load(namespaceName);
            if (MapUtils.isNotEmpty(localProperties)) {
                cacheItemMap.put(watchKey(namespaceName), config(localProperties));
            }
        }
        return cacheItemMap;
    }

    private Map<String, Object> config(Properties localProperties) {
        Map<String, Object> config = new HashMap<>();
        localProperties.forEach((key, value) -> config.put(key.toString(), value));
        return config;
    }


    private Properties load(String namespaceName) {
        try {
            String cacheFileName = getCacheFileName(namespaceName);
            FileSystemResource resource = new FileSystemResource(cacheFileName);
            return PropertiesLoaderUtils.loadProperties(resource);
        } catch (Exception e) {
            log.error("load cache file fail: {}", e.getMessage());
            return null;
        }
    }

    @Override
    public void notifyUpdate() {
        NotifyUpdateItemRequest request = NotifyUpdateItemRequest.builder()
                .envName(ENV_NAME)
                .clusterName(CLUSTER_NAME)
                .appName(APP_NAME)
                .namespaceNames(NAMESPACE_NAMES)
                .build();

        ResultDTO<NotifyUpdateItemResponse> resultDTO = RestUtil.post(UPDATE_URL, request, UPDATE_REFERENCE);
        if (ResultDTO.isSuccess(resultDTO)) {
            NotifyUpdateItemResponse data = resultDTO.getData();
            publisher.publish(new ItemChangeEvent(data.getItemMap()));
        }
    }

    private GetAllItemRequest getCacheRequest() {
        if (Objects.nonNull(cacheRequest)) return cacheRequest;
        return GetAllItemRequest.builder()
                .envName(ENV_NAME)
                .clusterName(CLUSTER_NAME)
                .appName(APP_NAME)
                .namespaceNameMap(getNamespaceNameMap())
                .build();
    }

    private Map<String, Long> getNamespaceNameMap() {
        Map<String, Long> namespaceNameMap = new HashMap<>();
        for (String namespaceName : NAMESPACE_NAMES.split(",")) {
            namespaceNameMap.put(namespaceName, null);
        }
        return namespaceNameMap;
    }

    private Map<String, Long> namespaceNameMap(Map<String, GetAllItemResponse.Namespace> namespaceMap) {
        if (MapUtils.isEmpty(namespaceMap)) return null;
        Map<String, Long> namespaceNameMap = new HashMap<>();
        namespaceMap.forEach((namespaceName, namespace) -> namespaceNameMap.put(namespaceName, namespace.getNamespaceId()));
        return namespaceNameMap;
    }

    @Override
    public void addListener(ItemChangeEventListener listener) {
        publisher.register(listener);
    }

}
